package com.example.javadocsearcher.util;

/**
 * Created with IntelliJ IDEA 2023.
 * Description:
 * User: homechen
 * Date: 2024-03-03
 * Time: 15:14
 */

import com.example.javadocsearcher.entity.DocInfo;
import com.example.javadocsearcher.entity.Weight;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 用于在内存中制作正排索引 + 倒排索引
 */
public class Index {
    // 使用数组的下标来表示 正排索引的 docId
    private List<DocInfo> forwardIndex = new ArrayList<>();

    // 新建两个锁，用于处理线程安全问题 ，也避免了锁冲突问题串行执行
    private Object lock1 = new Object();
    private Object lock2 = new Object();

    /**
     * 使用 hashmap 存储倒排索引
     * key：表示这个词  -  value 表示这个词所在的那些文档,通过 docId就可以找到所在文档
     */
    private Map<String, ArrayList<Weight>> invertedIndex = new HashMap<>();

    // 以json的格式保存到磁盘中，序列化 + 反序列化
    private ObjectMapper objectMapper = new ObjectMapper();

    // 保存索引到磁盘的路径
    private static String INDEX_PATH = null;

    static {
        if (Config.isOnline) {
            INDEX_PATH = "/home/tc/searcher_doc/";
        } else {
            INDEX_PATH = "D:/edge install/Java API";
        }
    }

    // 给定一个docId，在正排索引中找文档的相关信息
    public DocInfo getDocInfo(int docId) {
        return forwardIndex.get(docId); // 直接通过 docId进行获取到正排索引到文档
    }

    // 给定一个词，查看有哪些文档和这个词相关 - 在倒排索引中
    public List<Weight> getInverted(String term) {
        return invertedIndex.get(term);
    }

    // 将一个文件结构，添加为一个内存中的索引
    public void addDoc(String title, String url, String content) {
        // 1、先制作出正排索引
        DocInfo docInfo = buildForward(title, url, content);
        // 2、再通过正排索引到返回值制作出倒排索引
        buildInverted(docInfo);
    }


    private DocInfo buildForward(String title, String url, String content) {
        // 这里涉及多线程修改同一变量问题
        DocInfo docInfo = new DocInfo();
        docInfo.setTitle(title);
        docInfo.setUrl(url);
        docInfo.setContent(content);
        synchronized (lock1) {
            docInfo.setDocId(forwardIndex.size()); // 直接使用数组下标作索引 ID 即可
            // 添加到 正排索引列表中
            forwardIndex.add(docInfo);
        }

        return docInfo;
    }

    /**
     * 1、对文档的标题，进行分词遍历，并统计每个词出现的次数
     * 2、对文档的正文，进行分词遍历，并统计每个词出现的次数
     * 3、将统计的结果，存储到 hashmap中，key为这个词，value 为这个词出现的频率
     * 4、遍历汇总后的hashmap，依次更新到倒排索引中
     */
    private void buildInverted(DocInfo docInfo) {
        class WordCnt {
            private int titleCount; // 在标题中出现的次数  问我我  我在标题出现了两次
            private int contentCount; // 在正文中出现的次数
        }

        HashMap<String, WordCnt> wordCntHashMap = new HashMap<>();
        // 标题
        List<Term> terms = ToAnalysis.parse(docInfo.getTitle()).getTerms();
        for (Term term : terms) {
            //  拿到标题中的每一个词
            String word = term.getName(); // 转化为了小写，并去掉了后面的词性
            //  拿着这个词，去 hashmap 中找，没有找到就新增一个 WordCnt （title 为 1，
            //  contentCount 为 0）,找到了就将这个词的出现的标题数 + 1
            WordCnt wordCnt = wordCntHashMap.get(word);
            if (wordCnt == null) {
                WordCnt newWordCnt = new WordCnt();
                newWordCnt.titleCount = 1;
                newWordCnt.contentCount = 0;
                wordCntHashMap.put(word, newWordCnt);
            } else {
                wordCnt.titleCount += 1;
            }
        }

        // 正文
        terms = ToAnalysis.parse(docInfo.getContent()).getTerms();
        for (Term term : terms) {
            String word = term.getName();
            WordCnt wordCnt = wordCntHashMap.get(word);
            if (wordCnt == null) {
                WordCnt newWordCnt = new WordCnt();
                newWordCnt.titleCount = 0;
                newWordCnt.contentCount = 1;
                wordCntHashMap.put(word, newWordCnt);
            } else {
                wordCnt.contentCount += 1;
            }
        }

        // 获取到单词出现的次数
        for (Map.Entry<String, WordCnt> entry : wordCntHashMap.entrySet()) {
            // 获取这个词在hashMap中的倒排拉链，看是否为空
            synchronized (lock2) {
                List<Weight> invertedList = invertedIndex.get(entry.getKey());
                if (invertedList == null) {
                    // 此时就创建一个新的倒排拉链，插入到 hashMap中
                    ArrayList<Weight> newInvertedList = new ArrayList<>();
                    Weight weight = new Weight();
                    weight.setDocId(docInfo.getDocId());
                    // 设置权重为 title 次数 * 10 + content 次数
                    weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);
                    // 添加到新的拉链中
                    newInvertedList.add(weight);
                    // 再插入到倒排拉链中
                    invertedIndex.put(entry.getKey(), newInvertedList);
                } else {
                    // 此时不为空，直接 new 一个weight 加入到倒排拉链中
                    Weight weight = new Weight();
                    weight.setDocId(docInfo.getDocId());
                    weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);
                    invertedList.add(weight);
                }
            }
        }
    }

    // 将内存中的索引保存到磁盘文件中
    public void save() {
        long begin = System.currentTimeMillis();
        System.out.println("保存索引到磁盘开始！");
        // 先判断该路径是否存在
        File indexPathFile = new File(INDEX_PATH);
        if (!indexPathFile.exists()) {
            indexPathFile.mkdirs();
        }

        // 设置保存索引名
        File forwardIndexFile = new File(INDEX_PATH + "forwardIndex.txt");
        File invertedIndexFile = new File(INDEX_PATH + "invertedIndex.txt");

        // 将内存索引以json格式写入到磁盘中
        try {
            objectMapper.writeValue(forwardIndexFile, forwardIndex);
            objectMapper.writeValue(invertedIndexFile, invertedIndex);
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("保存索引到磁盘结束，Time：" + (end - begin) + " ms");
    }

    // 将磁盘中的索引加载到内存中 即 list + hashmap 中
    public void load() {
        long begin = System.currentTimeMillis();
        System.out.println("从磁盘中读取索引开始！");
        File forwardIndexFile = new File(INDEX_PATH + "forwardIndex.txt");
        File invertedIndexFile = new File(INDEX_PATH + "invertedIndex.txt");

        // 这里使用 TypeReference 这个工具将读出来的内容转化为一个对象，这里使用的是 匿名内部类
        try {
            forwardIndex = objectMapper.readValue(forwardIndexFile, new TypeReference<ArrayList<DocInfo>>() {
            });
            invertedIndex = objectMapper.readValue(invertedIndexFile, new TypeReference<Map<String, ArrayList<Weight>>>() {
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("从磁盘中读取索引结束，Time：" + (end - begin) + " ms");
    }

    /**
     * 测试将本地磁盘中的索引加载到内存中
     */
//    public static void main(String[] args) {
//        Index index = new Index();
//        index.load();
//        System.out.println("索引加载完毕！");
//    }

}
